В данной работе мы проводим анализ бизнес-показателей для развлекательного приложения Procrastinate Pro+. Цель исследования - Помочь компании минимизировать убытки и выйти в плюс.
Нашей задачей будет рассмотреть, почему компания терпит убытки. Для этого мы изучим следующее:
Для получения этой информации воспользуемся уже известными функциями для изучения метрик.
Загрузим все необходимые нам библиотеки
import pandas as pd
from datetime import datetime, timedelta
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
Загрузите данные о визитах, заказах и рекламных расходах из CSV-файлов в переменные.
Пути к файлам
/datasets/visits_info_short.csv. Скачать датасет;/datasets/orders_info_short.csv. Скачать датасет;/datasets/costs_info_short.csv. Скачать датасет.Изучите данные и выполните предобработку. Есть ли в данных пропуски и дубликаты? Убедитесь, что типы данных во всех колонках соответствуют сохранённым в них значениям. Обратите внимание на столбцы с датой и временем.
Для начала, загрузим всю необходимую для анализа информацию
path = 'D:\\Irina\\datasets\\'
visits = pd.read_csv(path + 'visits_info_short.csv')
orders = pd.read_csv(path + 'orders_info_short.csv')
costs = pd.read_csv(path + 'costs_info_short.csv')
display(visits.head())
visits.info()
| User Id | Region | Device | Channel | Session Start | Session End | |
|---|---|---|---|---|---|---|
| 0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
| 1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
| 2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
| 3 | 326433527971 | United States | Android | TipTop | 2019-05-01 00:29:59 | 2019-05-01 00:54:25 |
| 4 | 349773784594 | United States | Mac | organic | 2019-05-01 03:33:35 | 2019-05-01 03:57:40 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 309901 non-null int64 1 Region 309901 non-null object 2 Device 309901 non-null object 3 Channel 309901 non-null object 4 Session Start 309901 non-null object 5 Session End 309901 non-null object dtypes: int64(1), object(5) memory usage: 14.2+ MB
В таблице с визитами представлены следующие колонки:
user_id - хранит уникальные коды пользователей;region - страны, из которых пользователи посещали приложение;device - устройство, с которого пользователи заходили в приложение;channel - канал, через который пользователь "пришел" в приложение;session start - дата и время начала сессии;session end - дата и время конца сессии.Что можно сказать о таблице:
object, чтобы случайно не получить неожиданные результаты, но в данном случае этим можно пренебречь;Приступим к обработке:
#Переименуем столбцы, чтобы было удобно работать, можно было привести их к нижнему регистру,
#однако, нам нужно также привести названия к snake case
visits.columns = ['user_id','region','device','channel','session_start','session_end']
#Меняем типы данных
visits['session_start'] = pd.to_datetime(visits['session_start'])
visits['session_end'] = pd.to_datetime(visits['session_end'])
Проверим, совпадают ли даты в таблице с датами, указанными в описании прокета - 1 мая 2019 года - 27 октября 2013 года:
print('Первая дата посещения:', visits['session_start'].min(),
'\nПоследняя дата посещения:', visits['session_start'].max())
Первая дата посещения: 2019-05-01 00:00:41 Последняя дата посещения: 2019-10-31 23:59:23
Что же, начало сессий пользователей совпадает с теоритическими данными, а вот последняя дата начала сессии несколько выбивается. Но, в таблице скорее всего находятся данные повторных посещений пользователей, а это значит, что проблемы в данных нет. Но это мы узнаем позже, когда составим профили наших пользователей.
Перейдём к изучению пропусков и дубликатов
#Проверяем наличие пропусков в каждой колонке
visits.isna().sum()
user_id 0 region 0 device 0 channel 0 session_start 0 session_end 0 dtype: int64
#Проверим наличие явных дубликатов
print('Количество явных дубликатов в таблице:', visits.duplicated().sum())
Количество явных дубликатов в таблице: 0
#Проверяем наличие неявных дубликатов
print('Уникальные регионы:', visits['region'].unique())
print('Уникальные типы устройств пользователей:', visits['device'].unique())
print('Уникальные каналы привлечения:', visits['channel'].unique())
Уникальные регионы: ['United States' 'UK' 'France' 'Germany'] Уникальные типы устройств пользователей: ['iPhone' 'Mac' 'Android' 'PC'] Уникальные каналы привлечения: ['organic' 'TipTop' 'RocketSuperAds' 'YRabbit' 'FaceBoom' 'MediaTornado' 'AdNonSense' 'LeapBob' 'WahooNetBanner' 'OppleCreativeMedia' 'lambdaMediaAds']
В таблице отсутсвтуют пропуски и дубликаты, с этой таблицей уже можно спокойно работать. Приступим к следующей таблице.
Выведем таблицу с заказами и основную информацию о ней
display(orders.head())
display(orders.info())
#Проверим статистические показатели числовых данных
orders['Revenue'].describe()
| User Id | Event Dt | Revenue | |
|---|---|---|---|
| 0 | 188246423999 | 2019-05-01 23:09:52 | 4.99 |
| 1 | 174361394180 | 2019-05-01 12:24:04 | 4.99 |
| 2 | 529610067795 | 2019-05-01 11:34:04 | 4.99 |
| 3 | 319939546352 | 2019-05-01 15:34:40 | 4.99 |
| 4 | 366000285810 | 2019-05-01 13:59:51 | 4.99 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 40212 non-null int64 1 Event Dt 40212 non-null object 2 Revenue 40212 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 942.6+ KB
None
count 40212.000000 mean 5.370608 std 3.454208 min 4.990000 25% 4.990000 50% 4.990000 75% 4.990000 max 49.990000 Name: Revenue, dtype: float64
Здесь есть только три колонки:
user id - уникальный идентификатор пользователя, по которому мы сможем объединить таблицы для получения необходимой информацииevent dt - дата и время, в которое произошло событие (покупка)revenue - сумма покупкиКак и в предыдущем случае, следует привести названия колонок к единому и удобному стилю, поменять тип в колонке, содержащей дату и проверить качество данных. В столбце, содержащем сумму покупки, не отмечается аномальных данных.
#Приводим названия колонок к "хорошему" стилю
orders.columns = ['user_id','event_dt','revenue']
#Изменяем тип данных колонки, чтобы можно было работать с датами в ней
orders['event_dt'] = pd.to_datetime(orders['event_dt'])
#Проверим даты и здесь
print('Первая дата покупки:', orders['event_dt'].min(),
'\nПоследняя дата покупки:', orders['event_dt'].max())
Первая дата покупки: 2019-05-01 00:28:11 Последняя дата покупки: 2019-10-31 23:56:56
Здесь также первая покупка совпадает с теоритическими данными, а вот последняя дата выбивается, но это не значит, что в данные закралась ошибка. Посетители могли совершить покупку позднее привлечения. Продолжим предобработку.
#Проверяем наличие пропусков
orders.isna().sum()
user_id 0 event_dt 0 revenue 0 dtype: int64
print('Количество явных дубликатов в таблице:', orders.duplicated().sum())
Количество явных дубликатов в таблице: 0
В данном случае мы не можем проверить неявные дубликаты, потому что здесь нет текстовых данных, в которых могут быть ошибки. Поэтому, с обработкой данной таблицы мы закончили и пришло время приступить к следующей таблице.
Теперь перейдём к таблице с расходами на рекламу и получим основную информацию о ней.
display(costs.head())
display(costs.info())
costs['costs'].describe()
| dt | Channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 |
| 1 | 2019-05-02 | FaceBoom | 78.1 |
| 2 | 2019-05-03 | FaceBoom | 85.8 |
| 3 | 2019-05-04 | FaceBoom | 136.4 |
| 4 | 2019-05-05 | FaceBoom | 122.1 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null object 1 Channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: float64(1), object(2) memory usage: 42.3+ KB
None
count 1800.000000 mean 58.609611 std 107.740223 min 0.800000 25% 6.495000 50% 12.285000 75% 33.600000 max 630.000000 Name: costs, dtype: float64
Таблица содержит три колонки:
dt - дата проведения рекламной кампанииchannel - рекламный источникcosts - сколько стоила кампания в этот деньВ стоимости рекламы пока всё выглядит нормально, за исключением значения в 630 долларов. Либо в один из дней фирма решила провести масштабную рекламную акцию, либо это какая-то ошибка. К сожалению, мы не можем предположить что могло быть причиной такого и действительно ли можно считать это аномалией. Проведем преобразования и исследование данных по аналогии с предыдущими таблицами.
# В данном случае не нужно приводить к snake case
costs.columns = costs.columns.str.lower()
#Меняем тип данных и получаем дату
costs['dt'] = pd.to_datetime(costs['dt']).dt.date
#Проверим даты привлечения по проведению рекламных кампаний
print('Первая дата проведения кампании:', costs['dt'].min(),
'\nПоследняя дата проведения кампании:', costs['dt'].max())
Первая дата проведения кампании: 2019-05-01 Последняя дата проведения кампании: 2019-10-27
Здесь никаких аномалий нет, привлечение клиентов проводилось с 1 мая по 27 октября 2019 года, как и указано в описании данных.
#Смотрим пропуски
costs.isna().sum()
dt 0 channel 0 costs 0 dtype: int64
print('Количество явных дубликатов в таблице:', costs.duplicated().sum())
Количество явных дубликатов в таблице: 0
#Смотрим возможные опечатки для определения неявных дубликатов
print('Уникальные каналы привлечения:', costs['channel'].unique())
Уникальные каналы привлечения: ['FaceBoom' 'MediaTornado' 'RocketSuperAds' 'TipTop' 'YRabbit' 'AdNonSense' 'LeapBob' 'OppleCreativeMedia' 'WahooNetBanner' 'lambdaMediaAds']
Обработку данных мы завершили, теперь с тремя таблицами можно работать. Пора приступить к заданию необходимых для исследования инструментов.
Разрешается использовать функции, с которыми вы познакомились в теоретических уроках.
Это функции для вычисления значений метрик:
get_profiles() — для создания профилей пользователей,get_retention() — для подсчёта Retention Rate,get_conversion() — для подсчёта конверсии,get_ltv() — для подсчёта LTV.А также функции для построения графиков:
filter_data() — для сглаживания данных,plot_retention() — для построения графика Retention Rate,plot_conversion() — для построения графика конверсии,plot_ltv_roi — для визуализации LTV и ROI.Функция, позволяющая объединить исходные таблицы и получить полные данные каждого пользователя. Также добавляет признаки пользователей (покупатели/ не покупатели) и добавляет стоимость привлечения пользователя.
def get_profiles(visits, orders, costs):
# находим параметры первых посещений
profiles = (
visits.sort_values(by=['user_id', 'session_start'])
.groupby('user_id')
.agg(
{
'session_start': 'first',
'channel': 'first',
'device': 'first',
'region': 'first',
}
)
.rename(columns={'session_start': 'first_ts'})
.reset_index()
)
# Определяем дату первого посещения и первый день месяца в который это произошло
profiles['dt'] = profiles['first_ts'].dt.date
profiles['month'] = profiles['first_ts'].astype('datetime64[M]')
# добавляем признак платящих пользователей
profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())
# считаем количество уникальных пользователей с одинаковыми источником и датой привлечения
new_users = (
profiles.groupby(['dt', 'channel'])
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'unique_users'})
.reset_index()
)
# объединяем траты на рекламу и число привлечённых пользователей
costs = costs.merge(new_users, on=['dt', 'channel'], how='left')
# делим рекламные расходы на число привлечённых пользователей
costs['acquisition_cost'] = costs['costs'] / costs['unique_users']
# добавляем стоимость привлечения в профили
profiles = profiles.merge(
costs[['dt', 'channel', 'acquisition_cost']],
on=['dt', 'channel'],
how='left',
)
# стоимость привлечения органических пользователей равна нулю
profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)
return profiles
Функция для построения таблицы удержания и динамики удержания
def get_retention(
profiles,
sessions,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# добавляем столбец payer в передаваемый dimensions список
dimensions = ['payer'] + dimensions
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
sessions[['user_id', 'session_start']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу удержания
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# получаем таблицу динамики удержания
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
Функция для построения таблицы конверсии и динамики конверсии
def get_conversion(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# определяем дату и время первой покупки для каждого пользователя
first_purchases = (
purchases.sort_values(by=['user_id', 'event_dt'])
.groupby('user_id')
.agg({'event_dt': 'first'})
.reset_index()
)
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
result = result.fillna(0).cumsum(axis = 1)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим каждую «ячейку» в строке на размер когорты
# и получаем conversion rate
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу конверсии
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
if 'cohort' in dimensions:
dimensions = []
# получаем таблицу динамики конверсии
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
Функция для построения таблиц LTV, ROI, их динамики, а также для рассчета CAC
def get_ltv(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
)
# рассчитываем лайфтайм пользователя для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция группировки по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# строим «треугольную» таблицу выручки
result = df.pivot_table(
index=dims, columns='lifetime', values='revenue', aggfunc='sum'
)
# находим сумму выручки с накоплением
result = result.fillna(0).cumsum(axis=1)
# вычисляем размеры когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# объединяем размеры когорт и таблицу выручки
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# считаем LTV: делим каждую «ячейку» в строке на размер когорты
result = result.div(result['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result = result[['cohort_size'] + list(range(horizon_days))]
# восстанавливаем размеры когорт
result['cohort_size'] = cohort_sizes
# собираем датафрейм с данными пользователей и значениями CAC,
# добавляя параметры из dimensions
cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()
# считаем средний CAC по параметрам из dimensions
cac = (
cac.groupby(dims)
.agg({'acquisition_cost': 'mean'})
.rename(columns={'acquisition_cost': 'cac'})
)
# считаем ROI: делим LTV на CAC
roi = result.div(cac['cac'], axis=0)
# удаляем строки с бесконечным ROI
roi = roi[~roi['cohort_size'].isin([np.inf])]
# восстанавливаем размеры когорт в таблице ROI
roi['cohort_size'] = cohort_sizes
# добавляем CAC в таблицу ROI
roi['cac'] = cac['cac']
# в финальной таблице оставляем размеры когорт, CAC
# и ROI в лайфтаймы, не превышающие горизонт анализа
roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]
# возвращаем таблицы LTV и ROI
return result, roi
# получаем таблицы LTV и ROI
result_grouped, roi_grouped = group_by_dimensions(
result_raw, dimensions, horizon_days
)
# для таблиц динамики убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицы динамики LTV и ROI
result_in_time, roi_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
return (
result_raw, # сырые данные
result_grouped, # таблица LTV
result_in_time, # таблица динамики LTV
roi_grouped, # таблица ROI
roi_in_time, # таблица динамики ROI
)
Функция для "сглаживания" данных, чтобы строить более читаемые графики
def filter_data(df, window):
# для каждого столбца применяем скользящее среднее
for column in df.columns.values:
df[column] = df[column].rolling(window).mean()
return df
Функция для построения графиков удержания и динамики удержания:
def plot_retention(retention, retention_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 10))
# исключаем размеры когорт и удержание первого дня
retention = retention.drop(columns=['cohort_size', 0])
# в таблице динамики оставляем только нужный лайфтайм
retention_history = retention_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# если в индексах таблицы удержания только payer,
# добавляем второй признак — cohort
if retention.index.nlevels == 1:
retention['cohort'] = 'All users'
retention = retention.reset_index().set_index(['cohort', 'payer'])
# в таблице графиков — два столбца и две строки, четыре ячейки
# в первой строим кривые удержания платящих пользователей
ax1 = plt.subplot(2, 2, 1)
retention.query('payer == True').droplevel('payer').T.plot(
grid=True, ax=ax1
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.ylabel('Доля удержания')
plt.title('Удержание платящих пользователей')
# во второй ячейке строим кривые удержания неплатящих
# вертикальная ось — от графика из первой ячейки
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
retention.query('payer == False').droplevel('payer').T.plot(
grid=True, ax=ax2
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.ylabel('Доля удержания')
plt.title('Удержание неплатящих пользователей')
# в третьей ячейке — динамика удержания платящих
ax3 = plt.subplot(2, 2, 3)
# получаем названия столбцов для сводной таблицы
columns = [
name
for name in retention_history.index.names
if name not in ['dt', 'payer']
]
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == True').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.ylabel('Доля удержания')
plt.title(
'Динамика удержания платящих пользователей на {}-й день'.format(
horizon
)
)
# в чётвертой ячейке — динамика удержания неплатящих
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == False').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax4)
plt.xlabel('Дата привлечения')
plt.ylabel('Доля удержания')
plt.title(
'Динамика удержания неплатящих пользователей на {}-й день'.format(
horizon
)
)
plt.tight_layout()
plt.show()
Функция для построения графиков конверсии и динамики конверсии:
def plot_conversion(conversion, conversion_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 5))
# исключаем размеры когорт
conversion = conversion.drop(columns=['cohort_size'])
# в таблице динамики оставляем только нужный лайфтайм
conversion_history = conversion_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# первый график — кривые конверсии
ax1 = plt.subplot(1, 2, 1)
conversion.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.ylabel('Доля конверсии')
plt.title('Конверсия пользователей')
# второй график — динамика конверсии
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
columns = [
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
name for name in conversion_history.index.names if name not in ['dt']
]
filtered_data = conversion_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.ylabel('Доля конверсии')
plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
Функция для построения графиков LTV, ROI и их динамик, а также CAC. Для того, чтобы оценить окупаемость рекламы:
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):
# задаём сетку отрисовки графиков
plt.figure(figsize=(20, 10))
# из таблицы ltv исключаем размеры когорт
ltv = ltv.drop(columns=['cohort_size'])
# в таблице динамики ltv оставляем только нужный лайфтайм
ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
# стоимость привлечения запишем в отдельный фрейм
cac_history = roi_history[['cac']]
# из таблицы roi исключаем размеры когорт и cac
roi = roi.drop(columns=['cohort_size', 'cac'])
# в таблице динамики roi оставляем только нужный лайфтайм
roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
[horizon - 1]
]
# первый график — кривые ltv
ax1 = plt.subplot(2, 3, 1)
ltv.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.ylabel('Стоимость, $')
plt.title('LTV')
# второй график — динамика ltv
ax2 = plt.subplot(2, 3, 2, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in ltv_history.index.names if name not in ['dt']]
filtered_data = ltv_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
# третий график — динамика cac
ax3 = plt.subplot(2, 3, 3, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in cac_history.index.names if name not in ['dt']]
filtered_data = cac_history.pivot_table(
index='dt', columns=columns, values='cac', aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.ylabel('Стоимость, $')
plt.title('Динамика стоимости привлечения пользователей')
# четвёртый график — кривые roi
ax4 = plt.subplot(2, 3, 4)
roi.T.plot(grid=True, ax=ax4)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.legend()
plt.xlabel('Лайфтайм')
plt.ylabel('Стоимость, $')
plt.title('ROI')
# пятый график — динамика roi
ax5 = plt.subplot(2, 3, 5, sharey=ax4)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in roi_history.index.names if name not in ['dt']]
filtered_data = roi_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax5)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.xlabel('Дата привлечения')
plt.ylabel('Стоимость, $')
plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
После каждого пункта сформулируйте выводы.
У нас есть три таблицы, в которых записаны данные пользователей, данные посещений и покупок пользователей, а также траты на рекламу. Чтобы оценить прибыльность рекламных кампаний и от чего они зависят, необходимо составить профили пользователей. Для этого воспользуемся функцией get_profiles которую мы задали ранее, чтобы получить таблицу, в которой будет вся необходимая для анализа информация.
user_profiles = get_profiles(visits,orders,costs)
user_profiles.head()
| user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 599326 | 2019-05-07 20:58:57 | FaceBoom | Mac | United States | 2019-05-07 | 2019-05-07 20:58:57 | True | 1.088172 |
| 1 | 4919697 | 2019-07-09 12:46:07 | FaceBoom | iPhone | United States | 2019-07-09 | 2019-07-09 12:46:07 | False | 1.107237 |
| 2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 09:58:33 | False | 0.000000 |
| 3 | 22593348 | 2019-08-22 21:35:48 | AdNonSense | PC | Germany | 2019-08-22 | 2019-08-22 21:35:48 | False | 0.988235 |
| 4 | 31989216 | 2019-10-02 00:07:44 | YRabbit | iPhone | United States | 2019-10-02 | 2019-10-02 00:07:44 | False | 0.230769 |
Итак, мы получили таблицу, в которой есть:
user_id;first_ts;channel;device;region;dt;month;payer;acquisition_cost.Теперь мы сможем провести интересующий нас анализ окупаемости рекламы, но для начала поближе познакомимся с представленными данными.
#Получим граничные даты привлечения пользователей
min_acquisition_date = user_profiles['dt'].min()
max_acquisition_date = user_profiles['dt'].max()
print('Минимальная дата привлечения пользователей:', min_acquisition_date,
'\nМаксимальная дата привлечения пользователей:', max_acquisition_date)
Минимальная дата привлечения пользователей: 2019-05-01 Максимальная дата привлечения пользователей: 2019-10-27
Мы узнали, что пользователи привлекались в приложение с 1 мая по 27 октября 2019 года, как и было указано в описании данных. Теперь оценим, какие предпочтения у пользователей приложения в целом и что предпочитают платящие пользователи.
Чтобы оценить из каких стран, с каких устройств и через какие рекламные источники приходят пользователи составим соответствующие таблицы. А также рассчитаем конверсию по каждому из этих признаков (какой процент пользователей стал "покупателем" в зависимости от изучаемого признака).
#Cтроим таблицу зависимости от страны пользователя
country_users = user_profiles.groupby('region').agg({'user_id': 'nunique',
'payer': ['sum', 'mean']})
country_users.columns = ['user_id','payer_sum','payer_%']
country_users['payer_%'] = country_users['payer_%']*100
country_users = country_users.sort_values(by='payer_%', ascending = False)
#Таблица зависимости от устройства пользователя
device_users = user_profiles.groupby('device').agg({'user_id': 'nunique',
'payer': ['sum', 'mean']})
device_users.columns = ['user_id','payer_sum','payer_%']
device_users['payer_%'] = device_users['payer_%']*100
device_users = device_users.sort_values(by='payer_%', ascending = False)
#Таблица зависимости от рекламного источника
channel_users = user_profiles.groupby('channel').agg({'user_id': 'nunique',
'payer': ['sum', 'mean']})
channel_users.columns = ['user_id','payer_sum','payer_%']
channel_users['payer_%'] = channel_users['payer_%']*100
channel_users = channel_users.sort_values(by='payer_%', ascending = False)
# Выведем все таблицы
display('Платящие пользователи по регионам',
country_users,
'Платящие пользователи по устройствам',
device_users,
'Платящие пользователи по рекламным источникам',
channel_users)
'Платящие пользователи по регионам'
| user_id | payer_sum | payer_% | |
|---|---|---|---|
| region | |||
| United States | 100002 | 6902 | 6.901862 |
| Germany | 14981 | 616 | 4.111875 |
| UK | 17575 | 700 | 3.982930 |
| France | 17450 | 663 | 3.799427 |
'Платящие пользователи по устройствам'
| user_id | payer_sum | payer_% | |
|---|---|---|---|
| device | |||
| Mac | 30042 | 1912 | 6.364423 |
| iPhone | 54479 | 3382 | 6.207897 |
| Android | 35032 | 2050 | 5.851793 |
| PC | 30455 | 1537 | 5.046790 |
'Платящие пользователи по рекламным источникам'
| user_id | payer_sum | payer_% | |
|---|---|---|---|
| channel | |||
| FaceBoom | 29144 | 3557 | 12.204914 |
| AdNonSense | 3880 | 440 | 11.340206 |
| lambdaMediaAds | 2149 | 225 | 10.469986 |
| TipTop | 19561 | 1878 | 9.600736 |
| RocketSuperAds | 4448 | 352 | 7.913669 |
| WahooNetBanner | 8553 | 453 | 5.296387 |
| YRabbit | 4312 | 165 | 3.826531 |
| MediaTornado | 4364 | 156 | 3.574702 |
| LeapBob | 8553 | 262 | 3.063253 |
| OppleCreativeMedia | 8605 | 233 | 2.707728 |
| organic | 56439 | 1160 | 2.055316 |
Из полученных данных мы можем вынести следующие наблюдения:
Больше всего пользователей приложения из США - около ста тысяч. Из других стран в приложение пришли не более 20 тысяч пользователей. И покупателями охотнее всего становятся пользователи из США - конверсия составляет около 7%, в Европейских странах покупателями становятся всего около 4% пользователей. Это может быть связано с тем, что в самом приложении есть какие-то региональные особенности, или же рекламные кампании проводятся с бОльшим упором на США, нежели Европу.
Чаще всего приложением пользуются владельцы IPhone - 54 тысячи посетителей. На других устройствах примерно по 30 тысяч пользователей. Конверсия примерно одинакова для всех пользователей и составляет около 6%, кроме пользователей PC - там покупателями становятся только 5% пользователей. Значит приложение хорошо работает на всех платформах (возможно кроме PC) и окупаемость зависит от устройства не так сильно.
Среди рекламных источников можно выделить топ-5 по конверсии:
Конверсия тут составляет более 6%. Больше всего пользователей среди этих источников привлекли FaceBoom и TipTop. Однако, наиболее успешными можно считать кампании рекламных источников AdNonSense и lambdaMediaAds, так как они привлекают покупающих клиентов - у них высокая конверсия при небольшом притоке пользователей (данные рекламные источники привлекают меньше всего пользователей (менее 4 тысяч), однако большинство этих пользователей становятся покупателями). Но больше всего пользователей пришли без учёта рекламных кампаний - 56 тысяч, но у них низкая вероятность стать покупателями.
Таким образом можно сделать следующий вывод: Компании следует провести анализ отличий рынка США и Европы и повысить заинтересованность европейских пользователей, а также пересмотреть затраты на рекламные кампании. Так как больше всего посетителей приходит без стороннего привлечения, стоит обратить внимание, возможно сама компания недостаточно прозрачно раскрыла потенциал покупки приложения и у рекламных агенств получилось лучше показать преимущества покупки.
#Визуализируем доли платящих пользователей в зависимости от признаков
plt.figure(figsize=(23,10))
plt.suptitle('Доля платящих пользователей', fontsize=20)
(user_profiles.groupby('region')['payer'].sum()
.plot(kind='pie', y='region', autopct='%1.1f%%', ylabel=' ', ax=plt.subplot(1,3,1)))
plt.title('По странам', fontsize=16)
(user_profiles.groupby('device')['payer'].sum()
.plot(kind='pie', y='device', autopct='%1.1f%%', ylabel=' ', ax=plt.subplot(1,3,2)))
plt.title('По устройствам', fontsize=16)
(user_profiles.groupby('channel')['payer'].sum()
.plot(kind='pie', y='channel', autopct='%1.1f%%', ylabel=' ', ax=plt.subplot(1,3,3)))
plt.title('По рекламным каналам', fontsize=16);
Если оценивать по каждому источнику отдельно, что больше всего влияет на покупку приложения, можно сделать следующие выводы:
Напишите промежуточные выводы.
Оценим расходы на рекламу, сколько фирма всего потратила на рекламу и какие именно рекламные сервисы требуют больше всего денег.
print('Общая сумма расходов на маркетинг:', round(costs['costs'].sum(), 2))
Общая сумма расходов на маркетинг: 105497.3
costs.groupby('channel').agg({'costs':'sum'}).sort_values(by='costs',ascending=False)
| costs | |
|---|---|
| channel | |
| TipTop | 54751.30 |
| FaceBoom | 32445.60 |
| WahooNetBanner | 5151.00 |
| AdNonSense | 3911.25 |
| OppleCreativeMedia | 2151.25 |
| RocketSuperAds | 1833.00 |
| LeapBob | 1797.60 |
| lambdaMediaAds | 1557.60 |
| MediaTornado | 954.48 |
| YRabbit | 944.22 |
Больше половины денег, затраченных на рекламу уходят источникам TipTop и FaceBoom, однако данные источники приносят не так много платящих пользователей. Как мы помним из предыдущего пункта, источники AdNonSence и lambdaMediaAds работают более "качественно" принося не так много пользователей компании как первые два источника, но тех, кто более охотно переходит в разряд покупателей. К тому же с меньшими тратами за всё исследуемое время. На первый взгляд стоит пересмотреть растраты, так как есть возможность при меньших затратах получать похожие результаты. Рассмотрим далее, так ли это.
Оценим динамику расходов, для этого добавим в таблицу с расходами дополнительные колонки.
#Меняем тип данных, чтобы создать новые колонки
costs['dt'] = pd.to_datetime(costs['dt'])
#Создаем новые колонки
costs['week'] = costs['dt'].dt.isocalendar().week
costs['month'] = costs['dt'].dt.month
costs.head()
| dt | channel | costs | week | month | |
|---|---|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.3 | 18 | 5 |
| 1 | 2019-05-02 | FaceBoom | 78.1 | 18 | 5 |
| 2 | 2019-05-03 | FaceBoom | 85.8 | 18 | 5 |
| 3 | 2019-05-04 | FaceBoom | 136.4 | 18 | 5 |
| 4 | 2019-05-05 | FaceBoom | 122.1 | 18 | 5 |
Теперь построим графики, которые помогут оценить динамику изменения расходов по месяцам и по неделям.
plt.figure(figsize=(23,8))
plt.suptitle('Динамика изменения расходов во времени по рекламным источникам', fontsize=20)
(costs.pivot_table(index='week',columns='channel',values='costs',aggfunc='sum')
.plot(grid=True, ylabel='Стоимость, $', xlabel='Неделя', ax=plt.subplot(1,2,1)));
plt.title('По неделям', fontsize=16);
(costs.pivot_table(index='month',columns='channel',values='costs',aggfunc='sum')
.plot(grid=True, ylabel='Стоимость, $', xlabel='Месяц', ax=plt.subplot(1,2,2)));
plt.title('По месяцам', fontsize=16);
Мы можем отметить, что расходы на источники TipTop и FaceBoom имеют тенденцию к росту, фирма все больше и больше тратит деньги на два этих источника рекламы. При чем TipTop требует все больше расходов.
А что же с другими источниками? Уберем два наших "фаворита" по тратам и посмотрим, что же происходит с другими источниками.
plt.figure(figsize=(23,8))
plt.suptitle('Динамика изменения расходов во времени по рекламным источникам', fontsize=20)
(costs.query('channel != ["FaceBoom","TipTop"]')
.pivot_table(index='week',columns='channel',values='costs',aggfunc='sum')
.plot(grid=True, ylabel='Стоимость, $', xlabel='Неделя', ax=plt.subplot(1,2,1)));
plt.title('По неделям', fontsize=16);
(costs.query('channel != ["FaceBoom","TipTop"]')
.pivot_table(index='month',columns='channel',values='costs',aggfunc='sum')
.plot(grid=True, ylabel='Стоимость, $', xlabel='Месяц', ax=plt.subplot(1,2,2)));
plt.title('По месяцам', fontsize=16);
В данном случае, траты на остальные рекламные источники, кроме WahooNetBanner не меняются, или же медленно снижаются, особенно, если оценивать первый месяц/первые 4 недели наблюдений.
Скорее всего фирма оценивает эффективность рекламы по количеству привлеченных пользователей и вкладывается всё больше в те рекламные кампании, благодаря которым пришло много пользователей. Однако, для того, чтобы эта реклама окупилась, стоит оценивать конверсию пользователей в покупателей и отдавать предпочтение тем фирмам, которые привлекают именно покупателей.
Оценим, как распределилась стоимость привлечения пользователей для каждой рекламной кампании
#Удалим канал привлечения organic, потому что на него не идут расходы
cac = (user_profiles.query('channel !="organic"')
.groupby('channel')
.agg({'acquisition_cost':'mean'}).reset_index()
.rename(columns={'acquisition_cost':'cac'})
.sort_values(by='cac',ascending=False)
)
cac
| channel | cac | |
|---|---|---|
| 6 | TipTop | 2.799003 |
| 1 | FaceBoom | 1.113286 |
| 0 | AdNonSense | 1.008054 |
| 9 | lambdaMediaAds | 0.724802 |
| 7 | WahooNetBanner | 0.602245 |
| 5 | RocketSuperAds | 0.412095 |
| 4 | OppleCreativeMedia | 0.250000 |
| 8 | YRabbit | 0.218975 |
| 3 | MediaTornado | 0.218717 |
| 2 | LeapBob | 0.210172 |
avg_cac = user_profiles.query('channel != "organic"')
avg_cac = avg_cac['acquisition_cost'].mean()
print('Средняя стоимость привлечения одного клиента составляет {} долларов'.format(round(avg_cac,2)))
Средняя стоимость привлечения одного клиента составляет 1.13 долларов
Дороже всего стоит привлечение пользователей через источник TipTop- почти 3 доллара за человека, что в два раза превышает среднюю стоимость привлечения одного клиента. При этом, у остальных рекламных источников стоимость составляет примерно один доллар. Также, стоит отметить, что TipTop не самый эффективный источник, он привлекает не самое большое число пользователей и находится на 5 месте по конверсии в покупателей (примерно 9.7%). Кажется фирме стоит пересмотреть выбор рекламных источников. Но для начала проверим окупаемость, не будем "рубить с плеча".
Используя графики LTV, ROI и CAC, проанализируйте окупаемость рекламы. Считайте, что на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определите самостоятельно.
Напишите вывод, опишите возможные причины обнаруженных проблем и промежуточные рекомендации для рекламного отдела.
Перейдём к оценке окупаемости рекламы. Для этого отфильтруем наши данные:
profiles_filtred = user_profiles.query('channel != "organic"')
observation_date = datetime(2019,11,1).date()
horizon_days = 14
Оценим окупаемость рекламы при помощи метрик:
get_ltv.ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles_filtred, orders, observation_date, horizon_days)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
LTV растёт, как и положено, однако, можно отметить, что САС растёт быстрее, и таким образом, рекламные затраты совершенно не окупаются. В динамике LTV снижается, как и ROI, все указывает на то, что пользователи не окупаются через 2 недели. Кажется фирме стоит пересмотреть свои рекламные расходы.
Посмотрим, как охотно пользователи приложения становятся покупателями.
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_filtred, orders, observation_date, horizon_days)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Конверсия растёт и находится в нормальных пределах. С тем, чтобы пользователи становились покупателями проблем не отмечается.
Оценим, как быстро падает заинтересованность пользователей после привлечения
retention_raw, retention_grouped, retention_history = get_retention(
profiles_filtred, visits, observation_date, horizon_days
)
plot_retention(retention_grouped, retention_history, horizon_days)
Удержание пользователей снижается, неплатящие пользователи "уходят" быстрее и скорее всего не продержатся две недели. Платящие пользователи держатся гораздо стабильнее, что не удивительно, есть дополнительный "стимул". Но всё равно, нельзя сказать, что пользователи плохо удерживаются, всё выглядит нормально и даже неплохо.
А теперь перейдём к тому, как именно окупается реклама и что на это влияет, начнём с устройств
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Больше всего денег фирме приносят пользователи Mac,iPhone, Android, меньше всего - PC. Стоимость привлечения также растёт к 14 дню. Однако, из всех пользователей по прошествию второй недели окупаются только пользователи PC и то с трудом. Но на их привлечение и тратят меньше всего. Кажется стоит снизить затраты на привлечение пользователей с конкретными устройствами. Похоже, отдел маркетинга основывается на предположении, что пользователи продукции Apple охотнее тратят деньги. Но судя по графикам - ошибочно.
dimensions = ['device']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Конверсия в зависимости от устройств стабильная, охотнее всего платящими пользователями становятся пользователи Mac и iPhone. Хуже всего конвертируются пользователи PC. Также, конверсия пользователей РС больше подвергается сезонности. Скорее всего на девайсах фирмы Apple более удобная система оплаты, да и эти пользователи считаются более "платежеспособными".
dimensions = ['device']
retention_raw, retention_grouped, retention_history = get_retention(
profiles_filtred, visits, observation_date, horizon_days, dimensions=dimensions)
plot_retention(retention_grouped, retention_history, horizon_days)
Удержание же показывает картину, обратную конверсии - пользователи РС гораздо лучше удерживаются, однако и удержание носит для этих пользователей сезонный характер. А вот пользователи девайсов Apple удерживаются хуже. Возможно пользователи РС более избирательны и делают более обдуманный выбор в пользу покупки.
Как обстоят дела с окупаемостью рекламы в разных странах
dimensions = ['region']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Больше всего денег фирме приносят пользователи из США, что не удивительно, ведь больше всего пользователей именно из США. Однако, если оценить внимательнее, скорее всего это связано с тем, что огромные суммы денег тратятся именно на привлечение пользователей из США. Причину этого трудно выявить. Несмотря на все старания и на такое большое количество пользователей, реклама на привлечение в США не окупается. Эту стратегию также придётся пересмотреть.
dimensions = ['region']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Как и ожидалось, пользователи из США активнее становятся покупателями, они менее подверженны сезонности, чем пользователи из Европы. Видимо рекламные кампании невероятно эффективны в США, или же приложение больше ориентировано на пользователей именно этого региона (может обновления выходят в неудобное для европейских пользователей время).
dimensions = ['region']
retention_raw, retention_grouped, retention_history = get_retention(
profiles_filtred, visits, observation_date, horizon_days, dimensions=dimensions)
plot_retention(retention_grouped, retention_history, horizon_days)
А вот согласно данным удержания, пользователи США уходят также легко как и приходят. Среди Европейских пользователей привлекается больше "качественных" пользователей. Однако, удержание платящих пользователей США менее подверженно сезонности, чем в странах Европы.
А теперь переходим к самому важному и наиболее интересному пункту. Ранее мы выяснили, что на некоторые рекламные источники уходит слишком много денег. Теперь настала пора проверить, насколько же это оправданно.
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
Кажется мы нашли основную причину убытков компании - все деньги уходят на рекламные кампании, которые не приносят никакой выгоды. Кажется отделу маркетинга стоит пересмотреть рекламные стратегии и обратить внимание на конверсию пользователей, которую мы рассчитали ранее.
dimensions = ['channel']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Наибольшую конверсию приносят каналы FaceBoom, AdNonSense, lambdaMediaAds, TipTop, из них, менее всего подвержен сезонности канал FaceBoom. Хуже всего конвертируются пользователи каналов OppleCreativeMedia, LeapBob.
dimensions = ['channel']
retention_raw, retention_grouped, retention_history = get_retention(
profiles_filtred, visits, observation_date, horizon_days, dimensions=dimensions)
plot_retention(retention_grouped, retention_history, horizon_days)
Картина удержания поражает. Два самых успешных канала привлечения AdNonSense и FaceBoom приводят пользователей, которые навряд ли продержатся две недели. А вот удержание пользователей от канала TipTop показывает хорошие результаты. Однако это все равно мало оправдывает такие высокие затраты.
Проведем анализ, чтобы узнать какие рекламные агенства успешны в США и в Европе. Так можно будет дать корректные рекомендации и лучше оценить закономерности.
usa_users = profiles_filtred.query('region == "United States"')
eu_users = profiles_filtred.query('region != "United States"')
#Оценим окупаемость
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
usa_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
#Оценим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
usa_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_conversion(conversion_grouped, conversion_history, horizon_days)
#Оценим удержание
retention_raw, retention_grouped, retention_history = get_retention(
usa_users, visits, observation_date, horizon_days, dimensions=['channel'])
plot_retention(retention_grouped, retention_history, horizon_days)
В США в лидеры выбиваются рекламные фирмы YRabbit, MediaTornado, RocketSuperAds, они достаточно дешевые, у них хороший уровень удержания и они отлично себя окупают уже к третьему дню. Самый лучший рекламный источник здесь - RocketSuperAds, так как у него отличный уровень конверсии. Если снизить расходы на источник TipTop, возможно он начнет себя окупать.
#Оценим окупаемость
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
eu_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
#Оценим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
eu_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_conversion(conversion_grouped, conversion_history, horizon_days)
#Оценим удержание
retention_raw, retention_grouped, retention_history = get_retention(
eu_users, visits, observation_date, horizon_days, dimensions=['channel'])
plot_retention(retention_grouped, retention_history, horizon_days)
В Европе ситуация обстоит лучше, единственная фирма, которая не окупается - AdNonSense, она же не может удержать пользователей. В среднем здесь реклама окупается на 5 день привлечения. Фаворитом является фирма lambdaMediaAds - клиенты от этой фирмы приносят хороший доход, кампания окупается, хорошая конверсия пользователей и нормальное удержание. И она дешевле, чем AdNonSense. Кажется Европейскому отделу маркетинга стоит пересмотреть вложения средств.
Из проведенного анализа можно сделать вывод, что реклама не окупается.
FaceBoom, TipTop и AdNonSense - это самые дорогостоящие источники, которые имеют хорошую конверсию, однако у них есть ряд проблем - FaceBoom и AdNonSence приносят компании пользователей, которые не удерживаются в течение двух недель, а TipTop требует огромного количества расходов.Можно выделить ряд закономерностей, которые могут помочь отделу маркетинга справиться с возникшим кризисом
Основная причина неэффективности привлечения пользователей кроется в том, что выбрана неверная стратегия раскрутки приложения. Скорее всего, проблема состоит в том, что компания стремится вкладываться в то, что по их мнению "работает". Видимо оценка эффективности для отдела маркетинга в данном случае складывается из количества привлеченных пользователей. Возможно, они хотят взять количеством пользователей, а не "качеством".
На основании полученных результатов можно выделить следующие рекомендации:
TipTop, конверсия и удержание пользователей от этого канала в норме, однако траты на этот канал неустанно растут, при условии, что это не самый "успешный" канал привлечения. А также это самый дорогостоящий канал, который составляет более половины трат на рекламу (54 тысячи долларов из 105 тысяч в общем).США - да, это целевая аудитороия приложения, но эти расходы совершенно не окупаются, в отличие от привлечения пользователей из стран Европы. По распределению трат на привлечение по странам в США самая высокая стоимость привлечения, и также высокая конверсия, но удержание этих пользователей оставляет желать лучшего. Стоит потратить эти деньги на что-то другое. А также попробовать обратить внимание на фирму RocketSuperAds, так как она, пусть и уступает фирме TipTop, но имеет хорошие показатели в США.lambdaMediaAds, так как она показывает отличные результаты и требует меньше вложений.FaceBoom и AdNonSense - эти фирмы также в топе по стоимости проведения акций, и приносят хорошую конверсию, однако пользователи от этих фирм совершенно не удерживаются. Возможно дело в самих акциях - может они слишком приукрашены и реклама не соответсвтует действительности, либо дело в том, какой формат оплаты рекламы у этих фирм, стоит пересмотреть эту специфику. Скорее всего такой тип не подходит и слишком дорогостоящий, по сравнению с фирмами-конкурентами.Apple также не выглядит целесообразной. Здесь не наблюдается никаких проблем с привлечением и удержанием, но такой подход не окупается. Кажется стоит обратить внимание на то, как и каким образом проводятся акции для привлечения пользователей РС. Да, это может несколько снизить приток пользователей, но в настоящее время следует сделать упор на решение возникшего кризиса.Последний пункт не является обязательным, так как конверсия пользователей идет хорошо и скорее всего в технической части приложения нет никаких проблем.